Wyjaśnialne uczenie maszynowe – praca domowa 3

Katarzyna Koprowska

In [4]:
import pandas as pd
import numpy as np
import pickle
In [5]:
import warnings
warnings.filterwarnings("ignore")
In [6]:
import matplotlib.pyplot as plt

Wczytanie danych

Wykorzystanym zbirem danych jest Home Equity (HMEQ), zawierający informacje o 5960 klientach banku, którzy otrzymali kredyty hipoteczne.

Na podstawie zbioru próbowałam przewidzieć prawdopodobieństwo defaultu, czyli faktu, że klient będzie zalegał z płatnościami – określa to binarna zmienna BAD (1 oznacza default). Pozostałe 12 zmiennych opisuje m.in. historię kredytową aplikującego, historię zawodową oraz charakterystyki obecnej pożyczki.

Więcej informacji na temat danych można znaleźć pod linkiem https://www.kaggle.com/ajay1735/hmeq-data

In [7]:
hmeq = pd.read_csv("hmeq.csv", error_bad_lines=False)
In [13]:
hmeq_info = {'BAD' : 'client defaulted on loan 0 = loan repaid',
"LOAN" : "Amount of the loan request",
"MORTDUE" : "Amount due on existing mortgage",
"VALUE": "Value of current property",
"REASON": "DebtCon debt consolidation HomeImp = home improvement",
"JOBS" : "occupational categories",
"YOJ": "Years at present job",
"DEROG" : "Number of major derogatory reports",
"DELINQ": "Number of delinquent credit lines",
"CLAGE": "Age of oldest trade line in months",
"NINQ": "Number of recent credit lines",
"CLNO": "Number of credit lines",
"DEBTINC" : "Debt-to-income ratio"}

Przekształcenie danych nienumerycznych na dummy variables

In [6]:
from pandas.api.types import is_numeric_dtype
{column : is_numeric_dtype(hmeq[column]) for column in hmeq.columns}
Out[6]:
{'BAD': True,
 'LOAN': True,
 'MORTDUE': True,
 'VALUE': True,
 'REASON': False,
 'JOB': False,
 'YOJ': True,
 'DEROG': True,
 'DELINQ': True,
 'CLAGE': True,
 'NINQ': True,
 'CLNO': True,
 'DEBTINC': True}
In [7]:
set(hmeq['REASON'])
Out[7]:
{'DebtCon', 'HomeImp', nan}
In [8]:
set(hmeq['JOB'])
Out[8]:
{'Mgr', 'Office', 'Other', 'ProfExe', 'Sales', 'Self', nan}
In [9]:
hmeq = pd.concat([hmeq, pd.get_dummies(hmeq['REASON'], prefix='REASON', dummy_na=True)],axis=1)
hmeq = pd.concat([hmeq, pd.get_dummies(hmeq['JOB'], prefix='JOB', dummy_na=True)],axis=1)
hmeq.drop(['REASON', 'JOB'],axis=1, inplace=True)

Braki danych

In [10]:
hmeq.isna().sum()
Out[10]:
BAD                  0
LOAN                 0
MORTDUE            518
VALUE              112
YOJ                515
DEROG              708
DELINQ             580
CLAGE              308
NINQ               510
CLNO               222
DEBTINC           1267
REASON_DebtCon       0
REASON_HomeImp       0
REASON_nan           0
JOB_Mgr              0
JOB_Office           0
JOB_Other            0
JOB_ProfExe          0
JOB_Sales            0
JOB_Self             0
JOB_nan              0
dtype: int64
In [11]:
hmeq_nonan = hmeq.dropna()
In [12]:
X = hmeq_nonan.iloc[:, 1:]
y = hmeq_nonan.loc[:, "BAD"]
In [13]:
from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.35, random_state=42)
X_val, X_test, y_val, y_test = train_test_split(X_test, y_test, test_size=0.6, random_state=42)
In [14]:
for data in [X_train, X_test, X_val, y_train,  y_val, y_test]:
    data.reset_index(drop=True, inplace = True)
In [15]:
X_train.shape
Out[15]:
(2284, 20)
In [16]:
metrics = ["accuracy_train", "accuracy_test", "roc_auc_train", "roc_auc_test"]

Model – las losowy

In [17]:
from sklearn.ensemble import RandomForestClassifier
In [18]:
rf_final1 = pickle.load(open("final_nonan_rf.p", "rb"))

Sprawdzenie na zbiorze testowym

In [19]:
from sklearn.metrics import accuracy_score, roc_auc_score
In [20]:
results = {metric : {} for metric in ["accuracy_test", "roc_auc_test"]}
results["accuracy_test"]["RandomForest"] = (accuracy_score(y_test, rf_final1.predict(X_test)))
results["roc_auc_test"]["RandomForest"] = (roc_auc_score(y_test, rf_final1.predict_proba(X_test)[:,1]))
In [21]:
results = pd.DataFrame(results)
In [22]:
results
Out[22]:
accuracy_test roc_auc_test
RandomForest 0.941813 0.895652

Wyjaśnianie

TODO:

For the selected data set, train a predictive model for some selected observation from this dataset, calculate the model predictions for model (1) for an observation selected in (2), calculate the decomposition of model prediction using LIME / live / lime / localModel or similar technique (packages for R: live, live, localModel, iml, packages for python: lime). compare LIME decompositions for different observations in the dataset. How stable are these explanations? train a second model (of any class, neural nets, linear, other boosting) and find an observation for which LIME attributions are different between the models Comment on the results for points (4) and (5)

[2. for some selected observation from this dataset, calculate the model predictions for model (1)]

In [23]:
#np.random.seed(42)
#ind = np.random.randint(len(X_train.index)+1, size=1)[0]
ind = 6
obs = pd.DataFrame(X_test.iloc[ind, :]).T
In [24]:
y_test[obs.index].values
Out[24]:
array([1])
In [25]:
obs.size
Out[25]:
20
In [26]:
rf_final1.predict_proba(obs)
Out[26]:
array([[0.4199798, 0.5800202]])

[3. for an observation selected in (2), calculate the decomposition of model prediction using LIME / live / lime / localModel or similar technique (packages for R: live, live, localModel, iml, packages for python: lime).]

Przykładowe wyjaśnienie metodą LIME.

In [27]:
import lime
from lime import lime_tabular
In [28]:
from sklearn.datasets import load_iris
In [29]:
iris = load_iris()
In [30]:
explainer = lime.lime_tabular.LimeTabularExplainer(X_train.values, feature_names=list(X_train.columns), class_names=["GOOD","BAD"])
explainer_discretize = lime.lime_tabular.LimeTabularExplainer(X_train.values, feature_names=list(X_train.columns), class_names=["GOOD","BAD"], discretize_continuous=True)
In [49]:
y_train[ind]
Out[49]:
1
In [46]:
exp = explainer.explain_instance(X_train.values[ind], rf_final1.predict_proba)
exp.show_in_notebook(show_table=True, show_all=False)

W przypadku aplikującego opisanego powyżej (którego prawdziwa wartość zmiennej zależnej to 1, czyli popadnięcie w default) okazuje się, że na jego niekorzyść działa przede wszystkim

  • DEBTINC oznaczająca stosunek kwoty długów do dochodów wynosząca więcej niż 38.99.

Trochę mniejszy wpływ mają

  • DEROG, czyli dodatnia liczba złych pozycji w raporcie kredytowym
  • CLAGE, czyli najdłuższa linia kredytowa mniejsza niż 120.67 miesięcy.

Pozytywny wpływ miały z kolei zmienne:

  • DELINQ równa zero, oznaczająca brak linii kredytowych z zaległościami
  • REASON_nan równa 0, czyli powodem wzięcia kredytu jest konsolidacja obecnego zadłużenia lub remont domu
  • JOB_Self równa 0, czyli aplikant nie jest na samozatrudnieniu
  • JOB_Sales równa 0, czyli aplikant nie jest zatrudniony w sprzedaży.

[4. compare LIME decompositions for different observations in the dataset. How stable are these explanations?]

In [32]:
np.random.seed(42)
ind_good = np.random.randint(len(y_test[y_test==0]), size=4)
np.random.seed(42)
ind_bad = np.random.randint(len(y_test[y_test==1]), size=2)
obs_indexes = y_test[y_test[y_test==0].index[ind_good]].index.tolist()+ y_test[y_test[y_test==1].index[ind_bad]].index.tolist()
In [33]:
print(obs_indexes)
[109, 480, 294, 114, 601, 176]

LIME dla lasów losowych

In [34]:
for i in obs_indexes:
    exp = explainer.explain_instance(X_test.values[i], rf_final1.predict_proba)
    exp.show_in_notebook(show_table=True, show_all=False)

Powyższa analiza została przeprowadzaona dla czterech aplikujących z flagą GOOD oraz dwóch z flagą BAD, aby przeanalizować wpływ zmiennych dla różnych obserwacji.

Widzimy, że zmienna DEBTINC (stosunek kwoty długów do dochodów) ma konsekwentnie duży wpływ na predykcję niezależnie od wartości zmiennej odpowiedzi. Wygląda na to, że kluczowy jest punkt odcięcia równy 38.99: mniejsze wartości tej zmiennej wpływają pozytywnie (w stronę GOOD), większe - negatywnie (w stronę BAD).

Równie konsekwentnie istotny wpływ na wartość zmiennej odpowiedzi mają zmienne DEROG (liczba złych pozycji w raporcie kredytowym) i DELINQ (liczba linii kredytowych z zaległościami) : w przypadku obu z nich thresholdem powyżej którego wpływ ten zmienia się z pozytywnego na negatywny jest wartość 0.

W powyższych wizualizacjach dość często występują także zmienne dotyczące pracy: co ciekawe, brak pracy wpływa pozytywnie na prawdopodobieństwo bycia dobrym klientem wg analizowanego modelu, w przeciwieństwie do pracy m.in. w sprzedaży lub samozatrudnieniu.

Wielokrotnie pojawia się także zmienna CLAGE (najdłuższa linia kredytowa), dla której progiem, powyżej którego wpływ staje się pozytywny, jest wartość 178.03 (miesięcy).

Z pięciu na sześć obserwacji powyżej wynika również, że we wniosku kredytowym warto nie specyfikować powodu wzięcia kredytu (zmienna REASON_nan).

Ciekawą zmienną jest MORTDUE (kwota pozostała do spłacenia w posiadanym kredycie hipotecznym), która nie ma jednego progu, po przekroczeniu którego wpływ się zmienia. Przykładowo wpływ tej zmiennej na pierwszego klienta jest pozytywny mimo kwoty równej 74015.00, podczas gdy u drugiego wpływ jest negatywny pomimo niższej kwoty, wynoszącej 44696.00. Prawdopodobnie jest to związane z wartością zmiennej VALUE (wartość posiadanej nieruchomości), która dla pierwszej obserwacji wynosi 113766.00 oraz 57686.00 dla drugiej -- bardzo możliwe, że wysoka kwota pozostałego do spłacenia kredytu nie jest tak znacząca, jeśli wartość spłacanej nieruchomości znacznie ją przekracza.

W kilku obserwacjach występowała również zmienna NINQ, czyli liczba niedawno otwartych linii kredytowych, i konsekwentnie pozytywny wpływ miała wartość zero, czyli brak niedawno otwartych linii.

Pozostałe zmienne pojawiają się sporadycznie i nieregularnie.

[5. train a second model (of any class, neural nets, linear, other boosting) and find an observation for which LIME attributions are different between the models.]

In [35]:
from sklearn.ensemble import AdaBoostClassifier
from sklearn.tree import DecisionTreeClassifier
In [38]:
adaboost = AdaBoostClassifier(base_estimator = DecisionTreeClassifier(max_depth=7),n_estimators=150, random_state=42)
In [39]:
adaboost.fit(X_train, y_train)
Out[39]:
AdaBoostClassifier(algorithm='SAMME.R',
          base_estimator=DecisionTreeClassifier(class_weight=None, criterion='gini', max_depth=7,
            max_features=None, max_leaf_nodes=None,
            min_impurity_decrease=0.0, min_impurity_split=None,
            min_samples_leaf=1, min_samples_split=2,
            min_weight_fraction_leaf=0.0, presort=False, random_state=None,
            splitter='best'),
          learning_rate=1.0, n_estimators=150, random_state=42)
In [40]:
results = {metric : {} for metric in ["accuracy_test", "roc_auc_test"]}
results["accuracy_test"]["RandomForest"] = (accuracy_score(y_test, rf_final1.predict(X_test)))
results["roc_auc_test"]["RandomForest"] = (roc_auc_score(y_test, rf_final1.predict_proba(X_test)[:,1]))
results["accuracy_test"]["AdaBoost"] = (accuracy_score(y_test, adaboost.predict(X_test)))
results["roc_auc_test"]["AdaBoost"] = (roc_auc_score(y_test, adaboost.predict_proba(X_test)[:,1]))
pd.DataFrame(results)
Out[40]:
accuracy_test roc_auc_test
RandomForest 0.941813 0.895652
AdaBoost 0.945873 0.959918
In [42]:
ind_good, ind_bad
Out[42]:
(array([102, 435, 270, 106]), array([51, 14]))

LIME dla AdaBoost

In [43]:
for i in obs_indexes:
    exp = explainer.explain_instance(X_test.values[i], adaboost.predict_proba)
    exp.show_in_notebook(show_table=True, show_all=False)

Powyższa analiza została przeprowadzaona dla tych samych aplikujących, co poprzednio: czterech z flagą GOOD oraz dwóch z flagą BAD.

Zmienne DEBTINC (stosunek kwoty długów do dochodów), DEROG (liczba złych pozycji w raporcie kredytowym), DELINQ (liczba linii kredytowych z zaległościami) oraz CLAGE (najdłuższa linia kredytowa) pojawiają się z podobną częstotliwością i mają podobne progi warunkujące pozytywny lub negatywny wpływ.

Znowu dość często występują zmienne dotyczące pracy: większość wniosków z poprzedniego modelu aplikuje się także tutaj, poza samozatrudnieniem, które obecnie wpływa pozytywnie (na flagę GOOD). Oprócz tego dowiadujemy się, że pozytywny wpływ ma także zatrudnienie w biurze (JOB_Office), jako manager (JOB_Mgr) lub kierownik (JOB_ProfExe).

Jedną z większych różnic jest brak zmiennych NINQ i VALUE oraz rzadkie pojawienia się zmiennej MORTDUE, które we wcześniej analizowanym modelu pojawiała się dość często, co ponownie sugeruje potencjalny związek zmiennych VALUE i MORTDUE opisany w poprzedniej analizie.

Kilka razy pojawiły się natomiast zmienne nieobecne poprzednio:

  • YOJ, czyli liczba lat w obecnej pracy, w przypadku której okazuje się, że już 7.5 roku daje pozytywny wpływ, ale jeszcze lepiej jest pracować co najmniej 14 lat w tym samym miejsu
  • LOAN, czyli kwota wnioskowana, która ma pozytywny wpływ, jeśli jest mniejsza niż 12000
  • REASON_DebCon oraz REASON_HomeImp, czyli wyspecyfikowane we wniosku powody aplikowania o kredyt, odp. konsolidacja istniejacego zadłużenia (wpływ negatywny) oraz remont domu (wpływ pozytywny)

Pozostałe zmienne pojawiają się sporadycznie i nieregularnie.